Repository navigation
Conversation
…ects expect(value).resolves and .rejects waited on the internal JSPromise state and never called the value's then(). A Promise subclass that starts its work inside an overridden then(), such as Bun.SQL's Query or Bun.$'s ShellPromise, never settled, so the matcher spun the event loop forever and the test timeout could not interrupt it. A plain thenable was refused with "Expected promise". Adopt a pending promise or a thenable through a fresh native promise, which runs the spec resolve steps and calls the value's own then(), the same as await does. JSC skips that call for a promise with the built-in then. A settled promise is still read directly. The same path covers expect(fn).toThrow() on a returned promise and a custom matcher that returns a promise.
Let the adopter's resolve steps do the single Get(then), as await does. A value that is not a thenable fulfills the adopter at once, which is the signal for the Expected promise error. A then getter that throws now rejects instead of throwing from the matcher, the same as await. The hang guard test also asserts that the child exited on its own.
…s result Jest calls a function receiver and waits on what it returns. `.rejects` has done this since jest 26 (jestjs/jest#9817), `.resolves` since jest 30.0.4 (jestjs/jest#15714). The usage error now uses Jest's wording, "Expected a promise or a function returning a promise", and the settlement mismatch messages print the settled value instead of the receiver, which is not a promise when it is a thenable or a function. The `resolves` type unwraps a function receiver's return type, so `expect(async () => 1).resolves.toBe(1)` type-checks. The function receiver handling comes from #32944. Fixes #16144
… value CI runs the matchers with colors on, so the message reads `Received promise that resolved: \x1b[31m"settled to this"\x1b[0m`.
|
Pushed 104e0d5, a test-only change. CI runs the matchers with colors on, so the mismatch message reads |
|
Updated 4:06 PM PT - Aug 31st, 2026
✅ @robobun, your commit 104e0d548de019c73203c0ec43c598a168055e85 passed in 🧪 To try this PR locally: bunx bun-pr 41069That installs a local version of the PR into your bun-41069 --bun |
c6a9b4b to
0a65c0b
Compare
|
Main already has the change in this PR's title. #44833 (9e76acc) added it, and #16144 is closed.
This branch cannot land as it is. It has conflicts with main in four files. It also builds on the blocking wait in the test runner, which #44833 removed. Its base PR, #40996, is closed. Three parts of this PR are not on main. Each of them can be proposed again on top of main's code.
|
Problem
expect(fn).rejectsandexpect(fn).resolvesfail withExpected promisewhenfnis a function. Jest calls the function and waits on what it returns:.rejectssince jest 26 (feat: supportasync functions intoThrowjestjs/jest#9817),.resolvessince jest 30.0.4 (feat: supportasync functions intoBejestjs/jest#15714). Fixes Supportexpect(AsyncFunction).rejects#16144.process_promise(src/runtime/test_runner/expect.rs:510) hands the receiver straight to the thenable check. A function has nothen, so it fails as a non-promise.Fix
process_promisecalls a callable receiver, then waits on the return value throughthenable_to_wait_for(bun:test: settle thenables through their own then() in .resolves/.rejects #40996). A synchronous throw propagates as the test error, as in Jest. The usage error takes Jest's wording:Expected a promise or a function returning a promise.Received promise that resolved: "x"), not the receiver, which printed asPromise { <resolved> }.resolvesinbun-typesunwraps a function receiver's return type, soexpect(async () => 1).resolves.toBe(1)type-checks.test/js/bun/test/expect.test.js(new testresolves/rejects on a function returning a promise, function cases forexpect.rejectsTo/expect.resolvesTo),test/integration/bun-types/fixture/test.ts. Also the other expect suites intest/js/bun/test.Background
thenable_to_wait_for, an adopter promise that calls a value's ownthen(). Merge bun:test: settle thenables through their own then() in .resolves/.rejects #40996 first. This PR is the function receiver half of bun:test: accept thenables and functions in expect .resolves/.rejects #32944.expect.resolvesTo/expect.rejectsToare Bun-only asymmetric matchers. They reachprocess_promisethroughExpect_readFlagsAndProcessPromise, so insidetoEquala function-valued property that faces one of them is now called..resolvesfollows Jest 30.0.4. Vitest 4.1.9 calls a function receiver for.rejectsonly, so the.resolvescases inexpect.test.jsare gated on!isVitest.Notes
packages/expect/src/index.ts,makeResolveMatcher/makeRejectMatcher):typeof actual === 'function' ? actual() : actual, thenisPromise(actualWrapper)(any object or function with a callablethen). The usage error prints the original receiver, not its return value. This PR keeps both.() => 4) is the usage error, withReceived: [Function], as in Jest.process_promiseruns withsilent = true. A thrown error from the received function stays pending on the VM,Expect_readFlagsAndProcessPromisereturns false, and theRETURN_IF_EXCEPTIONthat bun:test: settle thenables through their own then() in .resolves/.rejects #40996 adds inmatchAsymmetricMatcherpropagates it instead of letting.notflip the FAIL to PASS. The newexpect.rejectsTo / resolvesTo propagate a sync throw from a received functiontest covers all four combinations.Received:, so anErrorprints with its stack, asexpect(new Error("x")).toBe(1)does today.rejectsstaysMatchers<unknown>. Theresolvestype isMatchers<Awaited<T extends (...args: any[]) => infer R ? R : T>>. Without it,expect(() => Promise.resolve(1)).resolves.toBe(1)isTS2769: No overload matches this call. The fixture checksMatchers<number>for a promise and for a function receiver.expect.test.jstest fails withExpected promiseon its first assertion, and the fixture reports four type errors without the.d.tschange.expect.test.js417 pass,bun-test.test.ts5 pass,expect-extend.test.js,jest-extended.test.js,spyMatchers.test.ts,expect-assertions.test.ts,mock-fn.test.js,bun-types.test.ts20 pass.BUN_JSC_validateExceptionChecks=1on the new tests reports nothing.thenable_to_wait_forskips an overriddenthen()on an already settled subclass. That helper belongs to bun:test: settle thenables through their own then() in .resolves/.rejects #40996, which documents the choice, and bun:test: wait on a Promise subclass through its own then() #41008 adds a sharedJSPromise::awaitablethat bun:test: settle thenables through their own then() in .resolves/.rejects #40996 plans to adopt.[review] gate passed · iteration 0 · 4 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 0
evidence per changed file